package sumsub

import (
	"bytes"
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"strconv"
	"time"

	"code.bydev.io/cht/customer/kyc-stub.git/pkg/bybit/kyc/common/enums"
	byoneconfig "code.bydev.io/cht/customer/kyc_admin_service/internal/config"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/config"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/constdata"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/helper"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/imgcompress"
	kae_dto "code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/kycadminevent/dto"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/kycprovider/dto"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/kycprovider/provider"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/teller"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/utils"
	"code.bydev.io/frameworks/byone/core/logc"
	"code.bydev.io/frameworks/byone/rest/httpc"
	"git.bybit.com/svc/go/pkg/bzap"
	"git.bybit.com/svc/mod/pkg/bsecret/bsecretsecret"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
	jsoniter "github.com/json-iterator/go"
	"github.com/spf13/cast"
	"go.uber.org/zap"
)

const (
	DefaultDomain = "https://test-api.sumsub.com"
	DefaultToken  = "tst:CM8S9KvxAB5QH0tBPNsMBB8J.xotfk2UX2WkHXRnUwT5EbTNcKy0ftcou"
	DefaultSecret = "Yh4NgTmR8Y6zmY91ynDck3Nk8OUCfx9w"
	RetryLimit    = 2
)

// SumSubProvider get applicant, images and webhooks
type SumSubProvider struct {
	Domain string
	Token  string
	Secret string
	logger *zap.Logger
	tell   provider.Teller
	imgCps imgcompress.ImgCps
}

// NewSumSubProvider create with option
func NewSumSubProvider(conf *byoneconfig.SumsubConfig, logger *zap.Logger, tell provider.Teller) *SumSubProvider {
	s := &SumSubProvider{
		Domain: conf.Domain,
		Token:  conf.Token,
		Secret: conf.Secret,
		logger: logger,
		tell:   tell,
	}
	if conf.Domain == "" {
		s.Domain = DefaultDomain
	} else {
		s.Domain = conf.Domain
	}

	if conf.Token == "" {
		s.Token = DefaultToken
	} else {
		s.Token = conf.Token
	}

	if conf.Secret == "" {
		s.Secret = DefaultSecret
	} else {
		s.Secret = conf.Secret
	}
	s.logger = logger
	s.tell = tell
	s.imgCps = imgcompress.New(logger, tell)
	return s
}

// GetToken get token for a user, and response to SDK
func (s *SumSubProvider) GetToken(ctx context.Context, uid string, ttl int, level dto.KYCLevel) (string, error) {
	type Response struct {
		Token string `json:"token"`
	}
	var err error
	p := fmt.Sprintf("/resources/accessTokens?userId=%s&levelName=Level%d&ttlInSecs=%d", uid, level, ttl)
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "GetToken[sumsub]:"+p, err)
		}
	}()
	b, err := s.request(p, "POST", nil)
	if err != nil {
		return "", err
	}

	var r Response
	err = json.Unmarshal(b, &r)
	if err != nil {
		return "", err
	}

	return r.Token, nil
}

func (s *SumSubProvider) ShareTokens(ctx context.Context, applicantID, forClientID string) (string, error) {
	type Response struct {
		Token string `json:"token"`
	}
	var err error
	p := fmt.Sprintf("/resources/accessTokens/-/shareToken?applicantId=%s&forClientId=%s", applicantID, forClientID)
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "ShareTokens[sumsub]:"+p, err)
		}
	}()
	b, err := s.request(p, "POST", nil)
	if err != nil {
		return "", err
	}

	var r Response
	err = json.Unmarshal(b, &r)
	if err != nil {
		return "", err
	}

	return r.Token, nil
}
func (s *SumSubProvider) ShareImport(ctx context.Context, shareToken string, userID string) (string, error) {
	type Response struct {
		Token string `json:"token"`
	}
	var err error
	p := fmt.Sprintf("/resources/applicants/-/import?shareToken=%s&userId=%s", shareToken, userID)
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "ShareImport[sumsub]:"+p, err)
		}
	}()
	b, err := s.request(p, "POST", nil)
	if err != nil {
		return "", err
	}

	var r Response
	err = json.Unmarshal(b, &r)
	if err != nil {
		return "", err
	}

	return r.Token, nil
}

// GetApplicantByID get applicant data with applicant id(uinque in subsum)
func (s *SumSubProvider) GetApplicant(ctx context.Context, applicantID string) (interface{}, error) {
	var (
		err error
		b   []byte
	)
	p := fmt.Sprintf("/resources/applicants/%s/one", applicantID)
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "GetApplicant[sumsub]:"+p, err)
		}
	}()

	i := 0
	for {
		b, err = s.request(p, "GET", nil)
		if i >= RetryLimit || err == nil {
			break
		}
		i++
	}
	if err != nil {
		return nil, err
	}

	a := new(SumsubApplicantInfo)
	err = json.Unmarshal(b, a)
	if err != nil {
		return nil, err
	}

	return a, nil
}

// GetApplicant get applicant data by hashed user id
func (s *SumSubProvider) GetApplicantByHashID(ctx context.Context, hashid string) (interface{}, error) {
	var err error
	p := fmt.Sprintf("/resources/applicants/-;externalUserId=%s/one", hashid)
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "GetApplicantByHashID[sumsub]:"+p, err)
		}
	}()
	b, err := s.request(p, "GET", nil)
	if err != nil {
		return nil, err
	}

	a := new(SumsubApplicantInfo)
	err = json.Unmarshal(b, a)
	if err != nil {
		return nil, err
	}

	return a, nil
}

// GetDocuments must get status first, then iterate images by GetDocument
func (s *SumSubProvider) GetDocuments(ctx context.Context, applicantID, inspectionID string, ch chan<- *dto.Document) error {
	var err error
	defer close(ch)
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "GetDocuments[sumsub]", err)
		}
	}()
	f := helper.TimeCost(s.logger, "SumSubProvider.getApplicantStatus")
	r, err := s.GetApplicantStatus(ctx, applicantID)
	f()
	if err != nil {
		return err
	}

	for _, id := range r.Identity.Imageids {
		f := helper.TimeCost(s.logger, "SumSubProvider.getDocumentIdentity")
		doc, err := s.getDocument(ctx, inspectionID, strconv.Itoa(id))
		f()
		if err != nil {
			return err
		}
		if s.logger != nil {
			s.logger.Info(fmt.Sprintf("filsize:%d", len(doc.Data)))
		}

		doc.Type = GetDocType(r.Identity.Iddoctype)
		select {
		case <-ctx.Done():
			return ctx.Err()
		case ch <- doc:
		}
	}

	for _, id := range r.ProofOfResidence.Imageids {
		f := helper.TimeCost(s.logger, "SumSubProvider.getDocumentProofOfResidence")
		doc, err := s.getDocument(ctx, inspectionID, strconv.Itoa(id))
		f()
		if err != nil {
			return err
		}
		if s.logger != nil {
			s.logger.Info(fmt.Sprintf("filsize:%d", len(doc.Data)))
		}
		doc.Type = GetDocType(r.ProofOfResidence.Iddoctype)
		select {
		case <-ctx.Done():
			return ctx.Err()
		case ch <- doc:
		}
	}

	for _, id := range r.Selfie.ImageIds {
		f := helper.TimeCost(s.logger, "SumSubProvider.getDocumentSelfie")
		doc, err := s.getDocument(ctx, inspectionID, strconv.Itoa(id))
		f()
		if err != nil {
			return err
		}
		if s.logger != nil {
			s.logger.Info(fmt.Sprintf("filsize:%d", len(doc.Data)))
		}
		doc.Type = GetDocType(r.Selfie.IDDocType)
		select {
		case <-ctx.Done():
			return ctx.Err()
		case ch <- doc:
		}
	}

	return nil
}

// getApplicantStatus get not only need an applicant’s status, but also information about documents
func (s *SumSubProvider) GetApplicantStatus(ctx context.Context, applicantID string) (*StatusResponse, error) {
	var (
		err error
		b   []byte
	)
	p := fmt.Sprintf("/resources/applicants/%s/requiredIdDocsStatus", applicantID)
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "getApplicantStatus[sumsub]:"+p, err)
		}
	}()

	i := 0
	for {
		b, err = s.request(p, "GET", nil)
		if i >= RetryLimit || err == nil {
			break
		}
		i++
	}
	if err != nil {
		return nil, err
	}

	res := new(StatusResponse)
	if err := json.Unmarshal(b, res); err != nil {
		return nil, err
	}

	return res, nil
}

func (s *SumSubProvider) GetApplicantModerationStatus(applicantID string) (*StatusResponse, error) {
	var err error
	p := fmt.Sprintf("/resources/moderationStates/-;applicantId=%s", applicantID)
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(context.Background(), "getApplicantStatus[sumsub]:"+p, err)
		}
	}()
	b, err := s.request(p, "GET", nil)
	if err != nil {
		return nil, err
	}

	var user4 interface{}

	res := new(StatusResponse)
	if err := json.Unmarshal(b, user4); err != nil {
		return nil, err
	}

	return res, nil
}

// getDocument get a image by id
// Binary content representing a document. The Content-Type response header more precisely describes the response mime-type.
func (s *SumSubProvider) getDocument(ctx context.Context, inspectionID, imageID string) (*dto.Document, error) {
	var err error
	p := fmt.Sprintf("/resources/inspections/%s/resources/%s", inspectionID, imageID)
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "getDocument[sumsub]:"+p, err)
		}
	}()
	httpCode, b, err := s.requestV2(ctx, p, "GET", nil, config.GetDownloadImageTimeout())
	if err != nil {
		return nil, err
	}
	// 图片不存在
	if httpCode == http.StatusNotFound {
		s.logger.Warn("getDocument[sumsub]: image not found", zap.String("inspectionID", inspectionID), zap.String("imageID", imageID))
		return nil, nil
	}
	// 服务端错误
	if httpCode >= http.StatusInternalServerError {
		s.logger.Error("getDocument[sumsub]: server error", zap.String("inspectionID", inspectionID), zap.String("imageID", imageID))
		return nil, constdata.ProviderServerError
	}

	mime := http.DetectContentType(b)
	if utils.IsValidMediaType(b) {
		doc := new(dto.Document)
		doc.ID = imageID
		doc.Data = b // raw response
		doc.Mime = mime
		return doc, nil
	} else {
		s.logger.Warn("getDocument[sumsub]: image type error", zap.String("inspectionID", inspectionID), zap.String("imageID", imageID), zap.String("mime", mime))
	}

	return nil, constdata.ImageTypeError
}

func (s *SumSubProvider) GetImageRaw(ctx context.Context, actionImg *dto.KYCActionImage) (*dto.Document, error) {
	return s.getDocument(ctx, actionImg.RecordID, actionImg.ImgID)
}

// Notify callback
func (s *SumSubProvider) Notify(ctx context.Context, data []byte) (interface{}, error) {
	var err error
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "Notify[sumsub]", err)
		}
	}()
	r := new(NotifyResponse)
	if err := json.Unmarshal(data, r); err != nil {
		return nil, err
	}

	return r, nil
}

// Resetting an applicant
func (s *SumSubProvider) ResetApplicant(applicantID string) (interface{}, error) {
	var err error
	p := fmt.Sprintf("/resources/applicants/%s/reset", applicantID)
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(context.Background(), "ResetApplicant[sumsub]:"+p, err)
		}
	}()
	b, err := s.request(p, "POST", nil)
	if err != nil {
		return nil, err
	}

	res := new(ResetApplicantResponse)
	if err := json.Unmarshal(b, res); err != nil {
		return nil, err
	}
	return res, nil
}

// X-App-Token - an App Token that you generate in our dashboard
// X-App-Access-Sig - signature of the request in the hex format (see below)
// X-App-Access-Ts - number of seconds since Unix Epoch in UTC
func (s *SumSubProvider) request(path, method string, body []byte) ([]byte, error) {
	ctx := context.Background()
	httpCode, respBody, err := s.requestV2(ctx, path, method, body, 10*time.Second)
	if err != nil {
		logc.Errorw(ctx, "s.requestV2 error", logc.Field("err", err))
		return nil, err
	}
	logc.Infow(ctx, "s.requestV2 info", logc.Field("respBody", string(respBody)))
	if httpCode != http.StatusOK {
		logc.Errorw(ctx, "s.requestV2 failed", logc.Field("httpCode", httpCode))
		if httpCode == http.StatusBadRequest || httpCode == http.StatusNotFound {
			return nil, constdata.ApplicantInvalidOrNotFound
		}
		return nil, errors.New("s.requestV2 failed, msg: " + string(respBody))
	}
	return respBody, nil
}

func (s *SumSubProvider) requestV2(ctx context.Context, path, method string, body []byte, t time.Duration) (int, []byte, error) {
	ts := fmt.Sprintf("%d", time.Now().Unix())
	headerMap := map[string]string{
		"X-App-Token":      s.Token,
		"X-App-Access-Sig": s.sign(ts, s.Secret, method, path, &body),
		"X-App-Access-Ts":  ts,
		"Accept":           "application/json",
		"Content-Type":     "application/json",
	}
	url := s.Domain + path
	// byone框架
	httpClient := &http.Client{
		Timeout: t,
	}
	httpService := httpc.NewServiceWithClient("httpService", httpClient, func(r *http.Request) *http.Request {
		for k, v := range headerMap {
			r.Header.Set(k, v)
		}
		return r
	})
	// 重新构造请求
	httpReq, err := http.NewRequest(method, url, bytes.NewBuffer(body))
	if err != nil {
		return 0, nil, err
	}
	response, err := httpService.DoRequest(httpReq)
	if err != nil {
		_ = teller.GlobTeller(ctx, "httpReq:"+url+" error:", err)
		logc.Errorw(ctx, "httpService.Do error", logc.Field("err", err))
		return 0, nil, err
	}
	defer func() {
		if err = response.Body.Close(); err != nil {
			logc.Errorw(ctx, "HttpC response.Body.Close error", logc.Field("err", err))
		}
	}()
	logc.Infow(ctx, "HttpC client.Do success", logc.Field("status", response.Status))
	respBody, err := ioutil.ReadAll(response.Body)
	if err != nil {
		logc.Errorw(ctx, "HttpC ioutil.ReadAll error", logc.Field("err", err))
		return 0, nil, err
	}
	if response.StatusCode != http.StatusOK {
		logc.Errorw(ctx, "client.Do respCode not success", logc.Field("respBody", string(respBody)))
		return response.StatusCode, respBody, nil
	}

	return response.StatusCode, respBody, nil
}

func (s *SumSubProvider) sign(ts string, secret string, method string, path string, body *[]byte) string {
	hash := hmac.New(sha256.New, []byte(secret))
	data := []byte(ts + method + path)

	if body != nil {
		data = append(data, *body...)
	}

	hash.Write(data)
	return hex.EncodeToString(hash.Sum(nil))
}

func (s *SumSubProvider) GetKYCAdminDetailFromNtf(ctx context.Context, idempotentID string, ntf *kae_dto.KYCEventNotify) (*dto.KYCAdminDetail, []*dto.KYCActionImage, error) {
	// 获取 applicant 数据
	applicant := new(SumsubApplicantInfo)
	err := jsoniter.UnmarshalFromString(ntf.RawResponse.One, applicant)
	if err != nil {
		return nil, nil, err
	}

	// 获取审核状态
	applicantStatus := new(StatusResponse)
	if err := jsoniter.UnmarshalFromString(ntf.RawResponse.Status, applicantStatus); err != nil {
		s.logger.Error("GetApplicantStatus error",
			zap.Any("idempotentID", idempotentID),
			zap.Any("RecordID", ntf.RecordID),
			zap.Error(err))
		return nil, nil, err
	}

	return s.pickupDetail(ctx, idempotentID, ntf, applicant, applicantStatus, nil)
}

func (s *SumSubProvider) GetKYCAdminDetail(ctx context.Context, idempotentID string, ntf *kae_dto.KYCEventNotify, firstPoi *dto.Identify) (*dto.KYCAdminDetail, []*dto.KYCActionImage, error) {
	if ntf.RawResponse.One != "" {
		return s.GetKYCAdminDetailFromNtf(ctx, idempotentID, ntf)
	}

	// 获取 applicant 数据
	app, err := s.GetApplicant(ctx, ntf.RecordID)
	if err != nil {
		bzap.LogError(s.logger.With(
			zap.String("idempotentID", idempotentID),
			zap.Any("ntf", ntf),
		), "GetApplicant", err)
		return nil, nil, err
	}
	applicant, ok := app.(*SumsubApplicantInfo)
	if !ok {
		return nil, nil, errors.New("sumsub get applicant result failed")
	}

	// 获取审核状态
	applicantStatus, err := s.GetApplicantStatus(ctx, ntf.RecordID)
	if err != nil {
		bzap.LogError(s.logger.With(
			zap.String("idempotentID", idempotentID),
			zap.String("RecordID", ntf.RecordID),
			zap.Any("ntf", ntf),
		), "GetApplicantStatus", err)
		return nil, nil, err
	}

	return s.pickupDetail(ctx, idempotentID, ntf, applicant, applicantStatus, firstPoi)
}

// GetKYCOnboardingPOIActionImages 获取 action 图片
func (s *SumSubProvider) GetKYCOnboardingPOIActionImages(ctx context.Context, memberID int64, accountID, recordID, idempotentID string) (actionImgs []*dto.KYCActionImage, err error) {
	app, errOne := s.GetApplicant(ctx, accountID)
	if errOne != nil {
		s.logger.Error("GetApplicant error",
			zap.Any("idempotentID", idempotentID),
			zap.Any("RecordID", accountID),
			zap.Error(errOne))
		err = errOne
		return
	}
	applicant, ok := app.(*SumsubApplicantInfo)
	if !ok {
		err = errors.New("sumsub get applicant result failed")
		return
	}
	// 获取状态
	applicantStatus, errStatus := s.GetApplicantStatus(ctx, accountID)
	if errStatus != nil {
		s.logger.Error("GetApplicantStatus error",
			zap.Any("idempotentID", idempotentID),
			zap.Any("RecordID", accountID),
			zap.Error(errStatus))
		err = errStatus
		return
	}

	actionImgs, _ = s.GetKYCActionImagesWithStatus(memberID, 1, accountID, idempotentID, applicant.GetInspectionID(), applicantStatus, false, false)

	return
}

func (s *SumSubProvider) pickupDetail(ctx context.Context, idempotentID string, ntf *kae_dto.KYCEventNotify,
	applicant *SumsubApplicantInfo, applicantStatus *StatusResponse, firstPoi *dto.Identify) (*dto.KYCAdminDetail, []*dto.KYCActionImage, error) {
	var (
		frontImgBucketStr, backImgBucketStr, faceImgBucketStr, docType string
	)
	// 是否第二证件
	isSecondary := ntf.IsSecondary()
	isSupplement := ntf.IsSupplement()
	// 获取数据图片并上传到 S3
	inspectionID := applicant.GetInspectionID()
	// 获取 action 图片
	actionImgs, _ := s.GetKYCActionImagesWithStatus(ntf.MemberID, ntf.Level, ntf.AccountID, idempotentID, inspectionID, applicantStatus, isSecondary, isSupplement)

	// 获取审核状态
	lvlName := applicant.GetLevelName()
	// 获取国家
	country := applicant.GetCountry()
	if ntf.Level == 2 {
		docType = applicantStatus.GetDocType4Lvl2(isSecondary, isSupplement)
		// BVN levelName 兜底
		if docType == "" && (lvlName == dto.Level2BVN.String() || lvlName == dto.Level2Secondary.String()) {
			docType = dto.DocumentTypeBill.String()
		}
		if applicantStatus.GetCountry4Lvl2() != "" {
			country = applicantStatus.GetCountry4Lvl2()
		}
	} else {
		docType = applicantStatus.GetDocType(isSecondary, isSupplement)
		// 迁移数据且 status 接口没有 Lv1 数据
		s.logger.Info("applicantStatus GetDocType", zap.Any("RecordID", ntf.RecordID), zap.String("docType", docType))
		if docType == "" && ntf.From == kae_dto.EventSourceMigrate {
			s.logger.Info("FixDocTypeNoLv1Info firstPoiType", zap.Any("RecordID", ntf.RecordID), zap.Any("firstPoi", firstPoi))
			docType = applicant.FixDocTypeNoLv1Info(ntf.Level, firstPoi, ntf.ResultDetail.FinalDecision)
			s.logger.Info("FixDocTypeNoLv1Info GetDocType", zap.Any("RecordID", ntf.RecordID), zap.String("docType", docType))
		}
		// BVN levelName 兜底
		if docType == "" && (lvlName == dto.Level1BVN.String() || lvlName == dto.Level2BVN.String() ||
			lvlName == dto.Level1SecondaryBVN.String() || lvlName == dto.Level2Secondary.String()) {
			docType = dto.DocumentTypeBVN.String()
		}
	}

	// 过滤 identity
	var (
		fltIdentity                      *dto.Identify
		docNumber, validUntil, issueDate string
	)
	if lvlName == dto.Level1BVN.String() ||
		(ntf.Level == 1 && (lvlName == dto.Level2BVN.String() || lvlName == dto.Level2Secondary.String()) && docType == dto.DocumentTypeBVN.String()) {
		docNumber = applicant.FixInfo.Tin
		issueDate = applicant.GetIssuedDate(docType)
	} else {
		if dto.KYCLevel(ntf.Level) == dto.Level1 {
			fltIdentity = applicant.GetFilteredIdentity(applicantStatus, ntf, firstPoi)
		} else {
			fltIdentity = applicant.GetFilteredIdentity4Lv2(applicantStatus)
		}
		if fltIdentity != nil {
			docNumber = fltIdentity.Number
			validUntil = fltIdentity.Validuntil
			issueDate = fltIdentity.IssuedDate
			if fltIdentity.Country != "" {
				country = fltIdentity.Country
			}
		} else {
			issueDate = applicant.GetIssuedDate(docType)
		}
	}

	var (
		providerDecision     dto.ReviewResult
		providerRejectReason string
	)
	// 旧数据补齐
	if ntf.From == kae_dto.EventSourceMigrate {
		providerDecision, providerRejectReason = applicant.GetReviewResultWithLvl(s.logger, applicantStatus, ntf.Level)
		s.logger.Debug("get providerDecision", zap.Int("providerDecision", int(providerDecision)),
			zap.Reflect("applicant", applicant))
	} else {
		providerDecision = applicant.GetReviewResult()
		providerRejectReason = applicant.GetRejectReasonsStr()
		s.logger.Debug("get providerDecision", zap.Int("providerDecision", int(providerDecision)),
			zap.Reflect("applicant", applicant))
	}
	s.logger.Debug("get first and last name", zap.String("GetFirstName", applicant.GetFirstName()),
		zap.String("GetLastName", applicant.GetLastName()), zap.String("GetLevelName", applicant.GetLevelName()))
	kd := &dto.KYCAdminDetail{
		ProviderDecision:     int8(providerDecision),
		ProviderRejectReason: providerRejectReason,
		DocType:              int8(provider.GetUnifiedDocType(GetDocType(docType).String(), dto.ProviderSumSub)),
		DocSubtype:           0,
		DocNumber:            docNumber,
		Country:              country,
		Nationality:          applicant.Nationality(),
		AddressCountry:       applicant.GetAddressCountry(),
		FirstName:            applicant.GetFirstName() + " " + applicant.GetMiddleName(),
		LastName:             applicant.GetLastName(),
		Dob:                  applicant.GetDOB(),
		DocDetail: dto.KYCAdminDocDetail{
			DocFront:       frontImgBucketStr,
			DocBack:        backImgBucketStr,
			PersonalNumber: "", // Sumsub 无此字段
		},
		IssuingDate: issueDate,
		ExpireDate:  validUntil,
		FaceDetail: dto.KYCAdminFaceDetail{
			FaceImage:         faceImgBucketStr,
			LivenessImage:     "", // Sumsub 无此字段
			SimilarityChecked: applicant.GetSimilarity(),
			ValidityChecked:   applicant.IsValidity(),
		},
		AddressDetail: dto.KYCAdminAddressDetail{
			Country:     applicant.GetAddressCountry(),
			State:       applicant.GetAddressState(),
			City:        applicant.GetAddressTown(),
			FullAddress: applicant.GetFullAddress(),
			PostalCode:  applicant.GetAddressPostCode(),
		},
		ClientDetail: dto.KYCAdminClientDetail{
			ClientIP: "",
			From:     "",
			Source:   applicant.GetApplicantPlatform(),
		},
	}
	if applicant.Reviewdate() != "" && ntf.From == kae_dto.EventSourceMigrate { // 只处理来自迁移的消息
		ft, errTm := time.Parse("2006-01-02 15:04:05+0000", applicant.Reviewdate())
		if errTm == nil {
			kd.ReviewAtE3 = ft.UnixNano() / int64(time.Millisecond)
		}
	}

	return kd, actionImgs, nil
}

// GetKYCImagesWithStatus 获取KYCImages
// Deprecated: use GetKYCActionImagesWithStatus instead
//func (s *SumSubProvider) GetKYCImagesWithStatus(ctx context.Context, idempotentID, inspectionID string,
//	applicantStatus *StatusResponse, isSecondary bool) (map[string]*dto.Document, error) {
//	ch := make(chan *dto.Document, 7) // 2 id_cards and 2 passports and 1 face_image
//	imgIds := make([]int, 0)
//	identityImgIds := applicantStatus.GetIdentityImageIds(isSecondary, false)
//	proofOfResidenceImgIds := applicantStatus.GetProofOfResidenceImageIds(isSecondary)
//	if len(identityImgIds) > 0 {
//		imgIds = append(imgIds, identityImgIds...)
//	}
//	if len(proofOfResidenceImgIds) > 0 {
//		imgIds = append(imgIds, proofOfResidenceImgIds...)
//	}
//	if len(applicantStatus.Selfie.ImageIds) > 0 {
//		imgIds = append(imgIds, applicantStatus.Selfie.ImageIds...)
//	}
//	if len(imgIds) < 1 {
//		return nil, nil
//	}
//	go func() {
//		e := s.GetAllDocuments(ctx, inspectionID, imgIds, ch)
//		if e != nil {
//			bzap.LogError(s.logger, "provider.GetDocuments error", e)
//		}
//	}()
//
//	if docs, e := uploadDocument(ctx, s.logger, idempotentID, ch); e != nil {
//		bzap.LogError(s.logger, "uploadDocument error", e)
//		return nil, e
//	} else {
//		s.logger.Debug("documents", zap.Any("docs", docs))
//		return docs, nil
//	}
//}

// GetKYCActionImagesWithStatus 获取KYCActionImages
func (s *SumSubProvider) GetKYCActionImagesWithStatus(memberID int64, level int, applicantID, idempotentID, inspectionID string,
	applicantStatus *StatusResponse, isSecondary, isSupplement bool) ([]*dto.KYCActionImage, error) {
	imgIds := make([]*dto.KYCActionImage, 0)
	identityImgIds := applicantStatus.GetIdentityImageIds(isSecondary, isSupplement)
	proofOfResidenceImgIds := applicantStatus.GetProofOfResidenceImageIds(isSecondary, isSupplement)
	if level == 1 {
		if len(identityImgIds) > 1 {
			imgIds = append(imgIds, &dto.KYCActionImage{
				IdempotentID: idempotentID,
				MemberID:     memberID,
				Provider:     enums.KYCProvider_PROVIDER_SUMSUB,
				AccountID:    applicantID,
				RecordID:     inspectionID,
				ImgID:        cast.ToString(identityImgIds[0]),
				ImgType:      dto.ImgTypeDocFront,
			}, &dto.KYCActionImage{
				IdempotentID: idempotentID,
				MemberID:     memberID,
				Provider:     enums.KYCProvider_PROVIDER_SUMSUB,
				AccountID:    applicantID,
				RecordID:     inspectionID,
				ImgID:        cast.ToString(identityImgIds[1]),
				ImgType:      dto.ImgTypeDocBack,
			})
		} else if len(identityImgIds) == 1 {
			imgIds = append(imgIds, &dto.KYCActionImage{
				IdempotentID: idempotentID,
				MemberID:     memberID,
				Provider:     enums.KYCProvider_PROVIDER_SUMSUB,
				AccountID:    applicantID,
				RecordID:     inspectionID,
				ImgID:        cast.ToString(identityImgIds[0]),
				ImgType:      dto.ImgTypeDocFront,
			})
		}
	} else {
		if len(proofOfResidenceImgIds) > 0 {
			imgIds = append(imgIds, &dto.KYCActionImage{
				IdempotentID: idempotentID,
				MemberID:     memberID,
				Provider:     enums.KYCProvider_PROVIDER_SUMSUB,
				AccountID:    applicantID,
				RecordID:     inspectionID,
				ImgID:        cast.ToString(proofOfResidenceImgIds[0]),
				ImgType:      dto.ImgTypeDocFront,
			})
		}
	}
	if len(applicantStatus.Selfie.ImageIds) > 0 {
		imgIds = append(imgIds, &dto.KYCActionImage{
			IdempotentID: idempotentID,
			MemberID:     memberID,
			Provider:     enums.KYCProvider_PROVIDER_SUMSUB,
			AccountID:    applicantID,
			RecordID:     inspectionID,
			ImgID:        cast.ToString(applicantStatus.Selfie.ImageIds[0]),
			ImgType:      dto.ImgTypeFace,
		})
	}

	return imgIds, nil
}

func (s *SumSubProvider) GetAllDocuments(ctx context.Context, inspectionID string, imgIds []int, ch chan<- *dto.Document) error {
	var err error
	defer close(ch)
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "GetDocuments[sumsub]", err)
		}
	}()

	for _, id := range imgIds {
		f := helper.TimeCost(s.logger, "SumSubProvider.getDocumentIdentity")
		doc, err := s.getDocument(ctx, inspectionID, strconv.Itoa(id))
		f()
		if err != nil {
			return err
		}
		if s.logger != nil {
			s.logger.Info(fmt.Sprintf("filsize:%d", len(doc.Data)))
		}
		doc.Type = "id_card"

		select {
		case <-ctx.Done():
			return ctx.Err()
		case ch <- doc:
		}
	}

	return nil
}

func (s *SumSubProvider) GetKYCImages(ctx context.Context, idempotentID, applicantID, inspectionID string) (map[string]*dto.Document, error) {
	ch := make(chan *dto.Document, 7) // 2 id_cards and 2 passports and 1 face_image
	go func() {
		defer helper.TimeCost(s.logger, "provider.GetDocuments")()
		e := s.GetDocuments(ctx, applicantID, inspectionID, ch)
		if e != nil {
			bzap.LogError(s.logger, "provider.GetDocuments error", e)
		}
	}()

	if docs, e := uploadDocument(ctx, s.logger, idempotentID, ch); e != nil {
		bzap.LogError(s.logger, "uploadDocument error", e)
		return nil, e
	} else {
		s.logger.Debug("documents", zap.Any("docs", docs))
		return docs, nil
	}
}

func uploadDocument(ctx context.Context, logger *zap.Logger, hashID string, ch <-chan *dto.Document) (map[string]*dto.Document, error) {
	var (
		secret = bsecretsecret.Secret([]byte(hashID))
		bucket = byoneconfig.GetAWSS3Config().Bucket
	)

	sess, err := helper.GetAWSS3Session()
	if err != nil {
		return nil, err
	}

	uploader := s3manager.NewUploader(sess)
	docs := make(map[string]*dto.Document)

	for doc := range ch {
		doc.Key = fmt.Sprintf("%s/%s", hashID, doc.ID) // hash(uid) && doc.id
		doc.Bucket = bucket
		docs[doc.ID] = doc
		body, e := secret.Seal(doc.Data)
		if e != nil {
			return nil, err
		}
		f := helper.TimeCost(logger, "UploadWithContext")
		_, err := uploader.UploadWithContext(ctx, &s3manager.UploadInput{
			Bucket: aws.String(bucket),
			Key:    aws.String(doc.Key),
			Body:   bytes.NewReader(body),
		})
		f()
		if err != nil {
			return nil, err
		}
	}

	return docs, nil
}

// ChangeLevelName This method updates required documents list according to the level provided.
// NOTE: Make sure to check an applicant status before changing the level.
//
//	You won't be able to do that if reviewStatus of the applicant is pending, queued or onHold.
func (s *SumSubProvider) ChangeLevelName(ctx context.Context, applicantID, levelName string) ([]byte, error) {
	var err error
	path := fmt.Sprintf("/resources/applicants/%s/moveToLevel?name=%s", applicantID, levelName)
	defer func() {
		if err != nil {
			_ = s.tell.KycAlarm(ctx, "ChangeLevelName[sumsub]:"+path, err)
		}
	}()

	b, err := s.request(path, http.MethodPost, nil)
	if err != nil {
		s.logger.Error("ChangeLevelName error", zap.String("applicantID", applicantID), zap.Error(err))
		return nil, err
	}

	s.logger.Info("ChangeLevelName", zap.String("applicantID", applicantID), zap.String("levelName", levelName), zap.String("b", string(b)))

	return b, nil
}
